package resource

import (
	reqContext "context"
	"net/http"
	"sync"

	"github.com/golang/protobuf/proto"
	contextImpl "git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/ichain/context"
	"git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/ichain/transactor"
	"git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/providers/ichain"
	ccomm "git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/util/comm"
	"git.cloud.inspur.com/ichain/ichain-sdk-go/thirdparty/ichain/protoutil"
	"github.com/pkg/errors"

	"git.cloud.inspur.com/ichain/ichain-protos-go/common"
	pb "git.cloud.inspur.com/ichain/ichain-protos-go/inode"
	"git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/common/errors/multi"
	"git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/common/errors/retry"
	"git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/common/logging"
)

var logger = logging.NewLogger("ichainsdk/resource")

type options struct {
	retry retry.Opts
}

type Opt func(opts *options)

func WithRetry(retry retry.Opts) Opt {
	return func(options *options) {
		options.retry = retry
	}
}

// CreateChain 请求创建/更新链配置 返回交易id
func CreateChain(reqCtx reqContext.Context, request CreateChainRequest, opts ...Opt) (ichain.TransactionID, error) {
	if request.Consenser == nil {
		return ichain.EmptyTransactionID, errors.New("missing consenser request parameter for the initialize chain")
	}

	if request.Name == "" {
		return ichain.EmptyTransactionID, errors.New("missing name request parameter for the new chain")
	}

	if request.Envelope != nil {
		return createChainFromEnvelope(reqCtx, request)
	}

	if request.Config == nil {
		return ichain.EmptyTransactionID, errors.New("missing envelope request parameter containing the configuration of the new chain")
	}

	if request.Signatures == nil {
		return ichain.EmptyTransactionID, errors.New("missing signatures request parameter for the new chain")
	}

	// 创建交易模块
	tx := transactor.NewTransactor(reqCtx, request.Name)
	txh, err := tx.NewTxHeader()
	if err != nil {
		return ichain.EmptyTransactionID, errors.WithMessage(err, "creation of transaction header failed")
	}

	optionsValue := getOpts(opts...)

	_, err = retry.NewInvoker(retry.New(optionsValue.retry)).Invoke(
		func() (interface{}, error) {
			return nil, createOrUpdateChain(reqCtx, txh, request)
		},
	)
	return txh.TransactionID(), err
}

func createChainFromEnvelope(reqCtx reqContext.Context, request CreateChainRequest) (ichain.TransactionID, error) {
	env, err := protoutil.ExtractSignedEnvelope(request.Envelope)
	if err != nil {
		return ichain.EmptyTransactionID, errors.WithMessage(err, "signed envelope not valid")
	}
	// Send request
	_, err = request.Consenser.SendBroadcast(reqCtx, env)
	if err != nil {
		return ichain.EmptyTransactionID, errors.WithMessage(err, "failed broadcast to consenser")
	}
	return ichain.EmptyTransactionID, nil
}

// GenesisBlockFromConsenser 生成创世区块
func GenesisBlockFromConsenser(reqCtx reqContext.Context, chainName string, consenser ichain.Consenser, opts ...Opt) (*common.Block, error) {
	optionsValue := getOpts(opts...)
	return retrieveBlock(reqCtx, []ichain.Consenser{consenser}, chainName, newSpecificSeekPosition(0), optionsValue)
}

// LastConfigFromConsenser 获取最新的配置区块
func LastConfigFromConsenser(reqCtx reqContext.Context, chainName string, consenser ichain.Consenser, opts ...Opt) (*common.Block, error) {
	logger.Debugf("chainConfig - start for chain %s", chainName)

	optionsValue := getOpts(opts...)

	block, err := retrieveBlock(reqCtx, []ichain.Consenser{consenser}, chainName, newNewestSeekPosition(), optionsValue)
	if err != nil {
		return nil, err
	}
	logger.Debugf("chainConfig - Retrieved newest block number: %d\n", block.Header.Number)

	lastConfig, err := GetLastConfigFromBlock(block)
	if err != nil {
		return nil, errors.Wrap(err, "GetLastConfigFromBlock failed")
	}
	logger.Debugf("chainConfig - Last config index: %d\n", lastConfig.Index)

	block, err = retrieveBlock(reqCtx, []ichain.Consenser{consenser}, chainName, newSpecificSeekPosition(lastConfig.Index), optionsValue)
	if err != nil {
		return nil, errors.WithMessage(err, "retrieve block failed")
	}
	logger.Debugf("chainConfig - Last config block number %d, Number of tx: %d", block.Header.Number, len(block.Data.Data))

	if len(block.Data.Data) != 1 {
		return nil, errors.New("apiconfig block must contain one transaction")
	}

	return block, nil
}

// JoinChain 发送加入链请求
func JoinChain(reqCtx reqContext.Context, request JoinChainRequest, targets []ichain.Inode, opts ...Opt) error {

	// 创世区块不得为空
	if request.GenesisBlock == nil {
		return errors.New("missing block input parameter with the required genesis block")
	}

	optionsValue := getOpts(opts...)

	// 创建加入链的合约调用请求
	cir, err := createJoinChainInvokeRequest(request.GenesisBlock)
	if err != nil {
		return errors.WithMessage(err, "creation of join chain invoke request failed")
	}

	var multiErrs multi.Errors
	var mutex sync.Mutex
	var wg sync.WaitGroup

	wg.Add(len(targets))

	for _, t := range targets {
		target := t
		go func() {
			defer wg.Done()
			if _, err := invokeContractWithTarget(reqCtx, cir, target, optionsValue); err != nil {
				mutex.Lock()
				multiErrs = append(multiErrs, err)
				mutex.Unlock()
			}
		}()
	}

	wg.Wait()

	return multiErrs.ToError()
}

// createOrUpdateChain 提交创建/更新链的请求
func createOrUpdateChain(reqCtx reqContext.Context, txh ichain.TransactionHeader, request CreateChainRequest) error {

	configUpdateEnvelope := &common.ConfigUpdateEnvelope{
		ConfigUpdate: request.Config,
		Signatures:   request.Signatures,
	}

	ctx, ok := contextImpl.RequestClientContext(reqCtx)
	if !ok {
		return errors.New("failed get client context from reqContext for Creating ChainHeader")
	}

	hash, err := ccomm.TLSCertHash(ctx.CryptoSuite(), ctx.EndpointConfig())
	if err != nil {
		return errors.WithMessage(err, "failed to get tls cert hash")
	}

	chainHeaderOpts := ichain.ChainHeaderOpts{
		TxnHeader:   txh,
		TLSCertHash: hash,
	}

	tx := transactor.NewTransactor(reqCtx, txh.ChainID())

	payload, err := tx.CreatePayload(common.HeaderType_CONFIG_UPDATE,
		chainHeaderOpts, configUpdateEnvelope)

	_, err = tx.BroadcastPayload(payload, []ichain.Consenser{request.Consenser})
	if err != nil {
		return errors.WithMessage(err, "SendEnvelope failed")
	}
	return nil
}

// QueryChains 查询链
func QueryChains(reqCtx reqContext.Context, inode ichain.Inode, opts ...Opt) (*pb.ChainQueryResponse, error) {

	if inode == nil {
		return nil, errors.New("inode required")
	}

	optionsValue := getOpts(opts...)

	cir := createChainsInvokeRequest()
	payload, err := invokeContractWithTarget(reqCtx, cir, inode, optionsValue)
	if err != nil {
		return nil, errors.WithMessage(err, "cscc.GetChains failed")
	}

	response := new(pb.ChainQueryResponse)
	err = proto.Unmarshal(payload, response)
	if err != nil {
		return nil, errors.Wrap(err, "unmarshal ChainQueryResponse failed")
	}
	return response, nil
}

// QueryInstalledContracts 查询已安装的合约
func QueryInstalledContracts(reqCtx reqContext.Context, inode ichain.Inode, opts ...Opt) (*pb.ContractQueryResponse, error) {

	if inode == nil {
		return nil, errors.New("inode required")
	}

	optionsValue := getOpts(opts...)

	cir := createInstalledContractsInvokeRequest()
	payload, err := invokeContractWithTarget(reqCtx, cir, inode, optionsValue)
	if err != nil {
		return nil, errors.WithMessage(err, "lscc.getinstalledcontracts failed")
	}

	response := new(pb.ContractQueryResponse)
	err = proto.Unmarshal(payload, response)
	if err != nil {
		return nil, errors.Wrap(err, "unmarshal ContractQueryResponse failed")
	}

	return response, nil
}

// InstallContract 安装合约
func InstallContract(reqCtx reqContext.Context, req InstallContractRequest, targets []ichain.Inode, opts ...Opt) ([]*ichain.TransactionProposalResponse, ichain.TransactionID, error) {

	if req.Name == "" {
		return nil, ichain.EmptyTransactionID, errors.New("contract name required")
	}
	if req.Path == "" {
		return nil, ichain.EmptyTransactionID, errors.New("contract path required")
	}
	if req.Version == "" {
		return nil, ichain.EmptyTransactionID, errors.New("contract version required")
	}
	if req.Package == nil {
		return nil, ichain.EmptyTransactionID, errors.New("contract package is required")
	}

	propReq := ContractInstallRequest{
		Name:    req.Name,
		Path:    req.Path,
		Version: req.Version,
		Package: &ContractPackage{
			Type: req.Package.Type,
			Code: req.Package.Code,
		},
	}

	tx := transactor.NewTransactor(reqCtx, ichain.SystemChain)

	prop, err := CreateContractInstallProposal(tx, propReq)
	if err != nil {
		return nil, ichain.EmptyTransactionID, errors.WithMessage(err, "creation of install contract proposal failed")
	}

	optionsValue := getOpts(opts...)

	resp, err := retry.NewInvoker(retry.New(optionsValue.retry)).Invoke(
		func() (interface{}, error) {
			return tx.SendTxProposal(prop, targets)
		},
	)
	if err != nil {
		return nil, ichain.EmptyTransactionID, err
	}

	return resp.([]*ichain.TransactionProposalResponse), prop.TxnID, nil
}

func invokeContractWithTarget(reqCtx reqContext.Context, request ichain.ContractInvokeRequest, target ichain.Inode, opts options) ([]byte, error) {

	targets := []ichain.Inode{target}

	tx := transactor.NewTransactor(reqCtx, ichain.SystemChain)

	tp, err := tx.CreateTxProposal(request)
	if err != nil {
		return nil, errors.WithMessage(err, "NewProposal failed")
	}

	resp, err := retry.NewInvoker(retry.New(opts.retry)).Invoke(
		func() (interface{}, error) {
			return tx.SendTxProposal(tp, targets)
		},
	)
	if err != nil {
		return nil, errors.WithMessage(err, "SendProposal failed")
	}

	tpr := resp.([]*ichain.TransactionProposalResponse)
	err = validateResponse(tpr[0])
	if err != nil {
		return nil, errors.WithMessage(err, "transaction proposal failed")
	}

	return tpr[0].ProposalResponse.GetResponse().Payload, nil
}

func validateResponse(response *ichain.TransactionProposalResponse) error {
	if response.Status != http.StatusOK {
		return errors.Errorf("bad status from %s (%d)", response.Endorser, response.Status)
	}

	return nil
}

func getOpts(opts ...Opt) options {
	var optionsValue options
	for _, opt := range opts {
		opt(&optionsValue)
	}
	return optionsValue
}
